package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"strconv"
	"sync"
	"time"
)

var (
	//新用户到来，通过该 Channel 进行登记
	enteringChannel = make(chan *User)
	//用户离开，通过该 channel 进行登记
	leavingChannel = make(chan *User)
	//广播专用的用户普通消息 channel
	//缓冲是尽可能避免出现异常情况堵塞，这里简单给了 8，具体值根据情况调整
	messageChannel = make(chan Message, 8)
)

// User 用户
type User struct {
	ID             int         //用户唯一标识，通过 GenUserID 函数生成
	Addr           string      //用户的 IP 地址和端口
	EnterAt        time.Time   //用户进入的时间
	MessageChannel chan string //当前用户发送消息的通道
	//需要给某个用户发送消息时，只需在该用户的 MessageChannel 中写入消息即可
}

// Message 给用户发送的消息
type Message struct {
	OwnerID int
	Content string
}

func (u *User) String() string {
	return fmt.Sprintf("%v, UID:%v, Enter At:%v", u.Addr, u.ID, u.EnterAt.Format("2006-01-02 15:04:05+8000"))
}

func main() {
	//在 net.Listen 时，没有指定 IP 地址，所以表示绑定到当前及其的 IP 地址上。
	listener, err := net.Listen("tcp", ":2020")
	if err != nil {
		panic(err)
	}

	go broadcaster()

	for {
		conn, err := listener.Accept() //建立连接
		if err != nil {
			log.Println(err)
			continue
		}

		go handlerConn(conn)
	}
}

// broadcaster 用于记录聊天室用户，并进行消息广播：
// 1.新用户进来；2.用户普通消息；3.用户离开
func broadcaster() {
	users := make(map[*User]struct{})

	for {
		select {
		case user := <-enteringChannel:
			//新用户进入
			users[user] = struct{}{}
		case user := <-leavingChannel:
			//用户离开
			delete(users, user)
			//避免 Goroutine 泄露
			close(user.MessageChannel)
		case msg := <-messageChannel:
			//给所有在线用户发送消息
			for user := range users {
				if user.ID == msg.OwnerID { //过滤自己的消息，自己发的消息自己不会收到
					continue
				}
				user.MessageChannel <- msg.Content
			}
		}
	}
}

// sendMessage 是在一个新的 Goroutine 中，
// 所以如果函数里的 ch 不关闭，则该 Goroutine 是不会退出的，
// 因此需要注意不关闭 ch 导致的 Goroutine 泄露问题
// <-chan string 为了限制在 sendMessage 函数只从 Channel 在读取数据，不允许往里面写入数据。
func sendMessage(conn net.Conn, ch <-chan string) {
	for msg := range ch {
		fmt.Fprintln(conn, msg)
	}
}

// 处理函数
func handlerConn(conn net.Conn) {
	defer conn.Close() //关闭连接

	//1.新用户进来，构建该用户的实例
	user := &User{
		ID:             GenUserID(),
		Addr:           conn.RemoteAddr().String(),
		EnterAt:        time.Now(),
		MessageChannel: make(chan string, 8),
	}

	//2.由于当前是一个新的 Goroutine 中进行读操作的，所以需要开一个 Goroutine 用于写操作。
	//读写 Goroutine 之间可以通过 Channel 进行通信
	go sendMessage(conn, user.MessageChannel)

	//3.给当前用户发送欢迎信息，向所有用户告知新用户的到来
	user.MessageChannel <- "Welcome, " + user.String()
	msg := Message{
		OwnerID: user.ID,
		Content: "user:`" + strconv.Itoa(user.ID) + "`has enter",
	}
	messageChannel <- msg

	//4.记录到全局用户列表中，避免用锁
	//注意这里和 3 的顺序不能反，否则会收到自己到来的消息提醒；当然，也可以做消息过滤处理
	enteringChannel <- user

	//踢出超时用户
	var userActive = make(chan struct{}) //定义了一个无缓冲通道的变量，用来接收用户活动的信号
	go func() {
		d := 5 * time.Minute      //定义了一个时长为5分钟的变量d，表示用户的超时时间。
		timer := time.NewTimer(d) //创建了一个定时器timer，它会在超过指定的时长后向其自身的通道 C 发送一个时间到期的事件。
		for {
			select {
			case <-timer.C: //当定时器的通道C接收到事件时，表示超时时间已经到达，执行这个case分支。
				conn.Close() //关闭该网络连接，将用户踢出（断开连接）。
			case <-userActive: //当 userActive 通道接收到事件时，表示有用户活动，执行这个 case 分支。
				timer.Reset(d) //重置定时器的时长为 d，相当于重新开始计时。
			}
		}
	}()

	//5.循环读取用户输入
	//并将用户信息发送给其他用户
	input := bufio.NewScanner(conn)
	for input.Scan() {
		msg.Content = strconv.Itoa(user.ID) + ":" + input.Text()
		messageChannel <- msg

		//用户活跃
		userActive <- struct{}{}
	}
	if err := input.Err(); err != nil {
		log.Println("读取错误：", err)
	}

	//6.用户离开
	leavingChannel <- user
	msg.Content = "user:`" + strconv.Itoa(user.ID) + "`has left"
	messageChannel <- msg
}

// 生成用户 ID
var (
	globalID int
	idLocker sync.Mutex
)

func GenUserID() int {
	idLocker.Lock()
	defer idLocker.Unlock()

	globalID++
	return globalID
}
